//uuid 生成器
function uuid() {
    var s = [];
    var hexDigits = "0123456789abcdef";
    for (var i = 0; i < 36; i++) {
        s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
    }
    s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
    s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
    s[8] = s[13] = s[18] = s[23] = "";

    var uuid = s.join("");
    return uuid;
}

//一个简单的连接客户端事件

//点对点消息回调
//onP2PMessage( userId, content) : {},

//加房间成功的回调
//onJoinRoomSuccess(roomId):{}

//加房间失败的回调
//onJoinRoomFailed(roomId):{}

//离开房间成功的回调
//onLeaveRoomSuccess(roomId):{}

//离开房间失败的回调
//onLeaveRoomFailed(roomId):{}


//房间消息回调
//onRoomMessage(roomId,  userId,  content) : {},

//业务异常回调
//onErrorMessage(code,content);

//连接最终结束
//onComplete: {},

//连接成功
//onConnected:{}

//重连中
//onConnecting:{}


//消息发送超时，这里msg，实际上属于底层发送的Command结构 ，目前暂时由业务层自行解析，后续有兴趣，可以再拆分到业务上，比如P2PMessageExpired等
//onMessageExpired(msg)

var MIMO_EXPIRE_IN_MILL = 7000; //7秒，则退场

var MIMO_MSG_DROP_EXPIRE_IN_MILL = 5000; //消息队列超过5秒的消息，会认定为失败

var MIMO_HEARTBEAT_TIMING = 3000;//心跳间隔

var MIMO_MSG_CHECKING_TIMING = 5000;//过期消息检查间隔


//初始状态
var MIMO_None = 0;

//正在连接中
var MIMO_Connecting = 1;

//连接成功
var MIMO_Connected = 2;

//断连，可能是一个中间状态
var MIMO_Disconnected = 3;

//终止状态，一般是退登
var MIMO_Terminate = 4;

//这个是客户端被迫强制中止的业务编号
var MIMO_Terminate_Codes = new Map([[1000, "Login Failed"], [1001, 'User Blocked'], [1003, 'Force Logout']]);

class MimoClient {

    //deivceId这里，是用于区分设备的，如果同一账户+同一设备信息，则系统会认为这是一个正常的重连操作。
    //如果账户相同，则设备信息与服务端当前维护的设备信息不一致，则认为是同一账户在多个设备之间的抢登
    constructor(url, user, token, deviceId, config) {

        this._origion = url;
        this._url = url;
        this._user = user;
        this._token = token;
        this._deviceId = deviceId;
        this._config = config;

        //构造请示的目标URL
        this._url += "?uid=" + user;
        this._url += "&token=" + token;
        this._url += "&deviceId=" + this._deviceId;
        this._url += "&terminal=WEB";
        this._url += "&version=1.2.0";

        this._msgCheckTimer = null; //发送消息列队检测器
        this._heartbeatTimer = null; //心跳器
        this._ws = null; //具体websocket实例


        //用于存储发送的数据, msgId -> data 的内容
        this._idToDataOnPost = new Map();

        //用于更新最近一次活跃时间
        this._lastActiveTime = 0;

        this._status = MIMO_None;

        console.log("URL -> " + this._url);
    }

    //发送点对点消息
    sendP2PMessage(targetPeer, content) {
        let p = {};
        p['id'] = uuid();
        p['type'] = 'P2P';
        p['target'] = targetPeer;
        p['content'] = content;
        p['ts'] = new Date().getTime();
        this.send(JSON.stringify(p));
    }

    //发送房间消息
    sendRoomMessage(roomId, content) {
        let r = {};
        r['id'] = uuid();
        r['type'] = 'P2Room';
        r['target'] = roomId;
        r['content'] = content;
        r['ts'] = new Date().getTime();
        this.send(JSON.stringify(r));
    }

    joinRoom(roomId) {
        let join = {};
        join['id'] = uuid();
        join['type'] = 'JoinRoom';
        join['target'] = roomId;
        join['ts'] = new Date().getTime();
        this.send(JSON.stringify(join));
    }

    leaveRoom(roomId) {
        let leave = {};
        leave['id'] = uuid();
        leave['type'] = 'LeaveRoom';
        leave['target'] = roomId;
        leave['ts'] = new Date().getTime();
        this.send(JSON.stringify(leave));
    }

    retrieveUnAck() {
        let retrieve = {};
        retrieve['id'] = uuid();
        retrieve['type'] = 'RetrieveUnAck';
        retrieve['ts'] = new Date().getTime();
        this.send(JSON.stringify(retrieve));
    }

    //最底层的消息发送入口
    send(content) {
        if (!this._ws) {
            alert('客户端已登出，请重新登录');
            return;
        }

        if (content && content.indexOf('HeartBeat') == -1) { //打印非心跳消息,同时填充到本地发送队列
            console.log('sending:' + content);
            let msg = $.parseJSON(content);
            if (!msg['ts']) {
                msg['ts'] = new Date().getTime();
            }
            console.log('id:' + msg['id'] + "  && ts:" + msg['ts']);
            if (msg['id']) {
                this._idToDataOnPost.set(msg['id'], msg);
                console.log('当前队列size:' + this._idToDataOnPost.size);
            }
        }

        if (this._checkWebSocketStatusBeforeSend()) {
            this._ws.send(new Blob([content]));
        } else {
            console.log("Current status Abnormal,try to reconnect...");
            this._init();
        }
    }

    // 处理来自服务端的消息
    handle(msg) {
        // ====================== 统一应答 ======================
        if (msg.type != 'Ack' && msg.type != 'Room2Peer') {
            let ack = {};
            ack['target'] = msg.id;
            ack['id'] = uuid();
            ack['type'] = 'Ack';
            this._ws.send(new Blob([JSON.stringify(ack)]));
        }

        // ====================== 根据消息类型具体处理 ======================
        // --------- 未应答消息检索结果 ---------
        if (msg.type === 'RetrieveUnAck' && msg.target && this._idToDataOnPost.delete(msg.target)) {
            if (msg.messages && msg.messages.length > 0) { // 消息包非空
                console.log('未应答消息包');
                // 递归，逐个处理
                msg.messages.forEach(function (element) {
                    this.handle(element);
                }.bind(this));
                // 发送下一次检索指令
                this.retrieveUnAck();
            }
        }

        // --------- ACK 请求成功 ---------
        if (msg.type === 'Ack' && msg.target) {
            let queuedCmd = this._idToDataOnPost.get(msg.target);
            if (queuedCmd) {
                this._idToDataOnPost.delete(msg.target);
                if (queuedCmd.type === 'JoinRoom') {
                    let roomId = queuedCmd['target'];
                    console.log('加房间成功的确认消息');
                    if (this._config && this._config.onJoinRoomSuccess && roomId) {
                        console.log('加房间成功回调');
                        this._config.onJoinRoomSuccess(roomId);
                    }
                } else if (queuedCmd.type === 'LeaveRoom') {
                    let roomId = queuedCmd['target'];
                    console.log('离开房间成功的确认消息');
                    if (this._config && this._config.onLeaveRoomSuccess && roomId) {
                        console.log('离开房间成功回调');
                        this._config.onLeaveRoomSuccess(roomId);
                    }
                }
            }
        }

        // --------- Error ---------
        if (msg.type === 'Error') {
            let queuedErrorCmd = this._idToDataOnPost.get(msg.id);
            // 请求失败
            if (queuedErrorCmd) {
                this._idToDataOnPost.delete(msg.id);
                if (queuedErrorCmd.type === 'JoinRoom') {
                    console.log('加房间失败');
                    let roomId = queuedErrorCmd['target'];
                    if (this._config && this._config.onJoinRoomFailed && roomId) {
                        console.log('加房间失败回调');
                        this._config.onJoinRoomFailed(roomId);
                    }
                } else if (queuedErrorCmd.type === 'LeaveRoom') {
                    console.log('离开房间失败');
                    let roomId = queuedErrorCmd['target'];
                    if (this._config && this._config.onLeaveRoomFailed && roomId) {
                        console.log('离开房间失败回调');
                        this._config.onLeaveRoomFailed(roomId);
                    }
                }
            }
            // 服务端主动推送的错误信息
            if (MIMO_Terminate_Codes.has(msg['code'])) { //如果遇到强制中止编码
                console.log('强制中止编码:' + msg['code'] + "-> 中止信息:" + MIMO_Terminate_Codes.get(msg['code']));
                this.logout(true);
            }
            // 回调至业务
            if (this._config && this._config.onErrorMessage) {
                this._config.onErrorMessage(msg['code'], msg['msg']);
            }
        }


        // --------- 其他业务消息回调 ---------
        if (msg.type === 'P2P' && this._config && this._config.onP2PMessage) {
            this._config.onP2PMessage(msg['from'], msg['content']);
        } else if (msg.type === 'Room2Peer' && this._config && this._config.onRoomMessage) {
            this._config.onRoomMessage(msg['roomId'], msg['from'], msg['content']);
        }

    }

    //当客户端主动发送消息时,用于检查当前实例的状态是否有效
    _checkWebSocketStatusBeforeSend() {
        let allow = false;
        if (!this._ws || this._status == MIMO_None) {
            console.log('连接尚未初始化');
            return allow;
        }

        switch (this._ws.readyState) {
            case WebSocket.CONNECTING:
                console.log('连接中,请稍候再发送');
                this._status = MIMO_Connecting;
                allow = false;
                break;
            case WebSocket.OPEN:
                this._status = MIMO_Connected;
                allow = true;
                break;
            case WebSocket.CLOSING:
            case WebSocket.CLOSED:
                console.log('连接已关闭');
                allow = false;
                this._status = MIMO_Disconnected;
                break;
            default:
                // this never happens
                break;
        }
        return allow;
    }

    //定时检测队列中的消息过期情况
    _msgDropCheck() {
        let self = this;
        this._idToDataOnPost.forEach(function (value, key, map) {
            let now = new Date().getTime();
            console.log('Queue key：' + key + " -> ts:" + value['ts']);
            if (self._status != MIMO_Terminate && now > value['ts'] && Math.abs(now - value['ts']) > MIMO_MSG_DROP_EXPIRE_IN_MILL) { //消息超时了
                map.delete(key);
                if (self._config && self._config.onMessageExpired) {
                    self._config.onMessageExpired(value);
                }
            }
        });
    }

    //内部心跳持续,此处为了方便，调整为定时发心跳
    //后续如果有时间，可以再做优化为full idle时再发心跳
    _heartBeat() {
        let now = new Date().getTime();

        if (this._ws && this._ws.readyState == WebSocket.OPEN && Math.abs(now - this._lastActiveTime) > MIMO_HEARTBEAT_TIMING) {
            let heartBeat = {};
            heartBeat['id'] = uuid();
            heartBeat['type'] = 'HeartBeat';
            this.send(JSON.stringify(heartBeat));
        }


        if (this._status != MIMO_Terminate && Math.abs(now - this._lastActiveTime) > MIMO_EXPIRE_IN_MILL) {
            console.log('心跳超时，启动重连');
            this._status = MIMO_Disconnected;
            this._init();
        }

    }

    //主动退登，特别注意，不能对注册事件做清空处理，主要原因是由于回调事件与ws本身的回调绑定在一起，而ws本身的回调状态滞后于回调触发
    logout(forceLogout) {

        //停掉定时器
        console.log('下线停掉心跳定时器');
        clearInterval(this._heartbeatTimer);
        this._heartbeatTimer = null;

        console.log('下线停掉过期消息定时器');
        clearInterval(this._msgCheckTimer);
        this._msgCheckTimer = null;

        //标记当前客户端的终止状态
        console.log('当前状态标记终止');
        this._status = MIMO_Terminate;

        if (!forceLogout) { //如果是非强制退登，则主动发送退登指令
            console.log('主动发送退登指令');
            let lg = {};
            lg['id'] = uuid();
            lg['type'] = 'Logout';
            this.send(JSON.stringify(lg));
        }

        //清空待发送的队列
        this._idToDataOnPost.clear();

        //关闭连接
        if (this._ws) {
            console.log('强制关闭 ws 连接');
            this._ws.close();
            this._ws = null;
        }
    }

    _init() {
        let self = this;

        if (self._status === MIMO_Connecting) { //如果已经在连接中，则直接返回
            console.log('已经在重连中了');
            return;
        }

        if (self._status === MIMO_Connected) { //如果已经连接成功了，则直接返回
            console.log('已经连接成功了');
            return;
        }

        if (self._status === MIMO_Terminate) { //如果连接过程已经结束,则直接返回
            console.log('连接过程已经结束');
            return;
        }


        //重置一切
        self._lastActiveTime = self._lastActiveTime ? self._lastActiveTime : 0;
        self._status = MIMO_Connecting;

        if ('WebSocket' in window) {
            if (self._ws && self._ws.readyState != WebSocket.CLOSED) {
                self._ws.close();
            }

            if (self._config && self._config.onConnecting) {
                self._config.onConnecting();
            }

            self._ws = new WebSocket(self._url);

            //连接成功建立的回调方法,启动定时心跳
            self._ws.onopen = function (event) {

                self._lastActiveTime = new Date().getTime();
                self._status = MIMO_Connected;

                if (self._config && self._config.onConnected) {
                    self._config.onConnected();
                }

                //心跳定时器,5秒一次
                if (!self._heartbeatTimer) {
                    self._heartbeatTimer = setInterval(self._heartBeat.bind(self), MIMO_HEARTBEAT_TIMING);
                }

                if (!self._msgCheckTimer) {
                    self._msgCheckTimer = setInterval(self._msgDropCheck.bind(self), MIMO_MSG_CHECKING_TIMING);
                }

                // 检索未应答消息
                self.retrieveUnAck();
            }

            //接收到消息的回调方法
            self._ws.onmessage = function (event) {
                self._lastActiveTime = new Date().getTime();
                self._status = MIMO_Connected;

                let reader = new FileReader();
                reader.readAsText(event.data, "UTF-8");
                reader.onload = function () {
                    let data = reader.result;
                    let msg = $.parseJSON(data);
                    if (msg.id) {
                        self.handle(msg);
                    }
                }
            };

            //连接关闭的回调方法
            self._ws.onclose = function () {
                console.log('onclose');
                self._status = MIMO_Disconnected;
                if (self._config && self._config.onComplete) {
                    console.log('call complete from close');
                    self._config.onComplete();
                }
            }

            //连接发生错误的回调方法
            self._ws.onerror = function (event) {
                console.log('onerror');
                self._status = MIMO_Disconnected;
                if (self._config && self._config.onComplete) {
                    console.log('call complete from error');
                    self._config.onComplete(event);
                }
            }

        } else {
            self._status = MIMO_None;
            alert('Not support websocket')
        }
    }

    //手动执行登陆动作
    connect() {
        if (!this._url) {
            throw new Error('实例对象没有初始化');
        }

        //为防止重复连接,如果原生的实例还在线，则做关闭清洗
        if (this._ws) {
            this._ws.close();
        }

        //于一秒后，启动连接动作
        setTimeout(this._init.bind(this), 1000);
    }

}
